#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <math.h>
#include <bps/bps.h>
#include <bps/dialog.h>
#include <sys/stat.h>
#include <sys/socket.h>
#include <netdb.h>
#include <unistd.h>
#include "tiles.h"


/**
 * The root folder where the tiles to be stored
 */
char root_dir[] = "shared/misc/Nafigator";

/**
 * Definition of the areas to cover up with tiles
 */
rect_t rect[] = {
    // Ottawa
    { {45.395351, -76.055921}, {45.281927, -75.522397}, 5, 14 }
};
#define NUM_RECTS (sizeof(rect) / sizeof(rect_t))

line_t line[] = {
    // Ottawa airport - Atlantic Beach
    { {45.321398, -75.665289}, {34.689575, -76.723818}, 8, 5 },
    { {45.321398, -75.665289}, {34.689575, -76.723818}, 9, 6 },
    { {45.321398, -75.665289}, {34.689575, -76.723818}, 10, 7 }
};
#define NUM_LINES (sizeof(line) / sizeof(line_t))


/**
 * Statistics variables
 */
int tiles_loaded = 0;       // number of new successfully loaded tiles
int tiles_failed = 0;       // number of tiles failed to load
int mkdir_failed = 0;       // number of failed attempts to create directory
int num_retries;            // number of unsuccessful attempts to retrieve tile

/**
 * Common buffer used to prepare the path to a file/folder
 */
char path[256];

/**
 * Host URL of the tile server
 */
char host[256];

/**
 * Buffer to prepare HTTP GET request
 */
char get[256];

/**
 * Buffer to receive data from the socket
 */
unsigned char buffer[DEFAULT_BUFLEN];

/**
 * Socket handle
 */
SOCKET s = INVALID_SOCKET;

/**
 * Dialog handle
 */
dialog_instance_t dialog;


/**
 * @brief Display the dialog with the specified message
 *
 * @return none
 */
void dialog_set_title(char *msg) {
    if(dialog) {
        dialog_set_title_text(dialog, msg);
        dialog_update(dialog);
    }
}

/**
 * @brief Display the dialog with the specified message
 *
 * @return none
 */
void dialog_show_message(char *msg) {
    if(dialog) {
        dialog_set_alert_message_text(dialog, msg);
        dialog_update(dialog);
    }
}

/**
 * @brief Cleanup of the dialog
 *
 * @return none
 */
void dialog_cleanup(void) {
    if(dialog) {
        dialog_destroy(dialog);
    }
    dialog = NULL;
}

/**
 * @brief Initialisation of the dialog
 *
 * @return none
 */
void dialog_init(void)
{
    dialog_create_alert(&dialog);
    dialog_set_cancel_required(dialog, true);
    dialog_set_title_text(dialog, "Tiles");
    dialog_set_alert_message_text(dialog, "");
    dialog_show(dialog);
}

/**
 * @brief Close HTTP socket
 *
 * @return none
 */
void sock_close(void)
{
    if(s != INVALID_SOCKET) {
        close(s);
        s = INVALID_SOCKET;
    }
}

/**
 * @brief Open HTTP socket
 *
 * @return EXIT_SUCCESS or EXIT_FAILURE
 */
int sock_open(void)
{
    static char server = SERVER_FIRST;
    struct addrinfo hints, *res, *res0;
    int error;

    sprintf(host, FORMAT_HOST, server++);
    if(server > SERVER_LAST) {
        server = SERVER_FIRST;
    }

    memset(&hints, 0, sizeof(hints));
    hints.ai_family = PF_UNSPEC;
    hints.ai_socktype = SOCK_STREAM;
    error = getaddrinfo(host, DEFAULT_PORT, &hints, &res0);
    if(error) {
        LOG("Resolution of \"%s\" failed, %s", host, gai_strerror(error));
        return EXIT_FAILURE;
    }

    for(res = res0; res; res = res->ai_next) {
        s = socket(res->ai_family, res->ai_socktype, res->ai_protocol);
        if(s == INVALID_SOCKET) {
            //LOG("socket() failed");     // debug
            continue;
        }

        if(connect(s, res->ai_addr, res->ai_addrlen) != EXIT_SUCCESS) {
            close(s);
            s = INVALID_SOCKET;
            //LOG("connect() failed");    // debug
            continue;
        }

        break;  // okay we got one
    }
    freeaddrinfo(res0);

    if(s == INVALID_SOCKET) {
        LOG("Connection to \"%s\" failed", host);
        return EXIT_FAILURE;
    }

    return EXIT_SUCCESS;
}

/**
 * @brief Parse HTTP response header
 *
 * @return EXIT_SUCCESS or EXIT_FAILURE
 */
int parse_http_response(int *content_lenght, unsigned char **data)
{
    static char http_resp[] = "HTTP/";  // http response must begin with it, e.g. "HTTP/1.1 200 OK"
    int vmaj, vmin;     // major and minor version of http protocol, used by the server
    int res = 0;        // http response code
    char *ptr;

    if(strncmp((char*)buffer, http_resp, sizeof(http_resp) - 1)) {
        LOG("Bad http response");
        return EXIT_FAILURE;
    }

    sscanf((char*)buffer, "HTTP/%d.%d %d", &vmaj, &vmin, &res);
    if(res != 200) {
        LOG("http response code %d is not OK", res);
        return EXIT_FAILURE;
    }

    if(!(ptr = strstr((char*)buffer, "Content-Length:"))) {
        LOG("Content-Length not found");
        return EXIT_FAILURE;
    }
    sscanf(ptr, "Content-Length: %d", content_lenght);

    if(!(ptr = strstr((char*)buffer, "\r\n\r\n"))) {
        LOG("end of header not found");
        return EXIT_FAILURE;
    }
    *data = (unsigned char*)ptr + 4;

    return EXIT_SUCCESS;
}

/**
 * @brief Retrieving the specified tile
 *
 * @return none
 */
void load_tile(int zoom, int x, int y)
{
    FILE *f;
    int res;
    int content_lenght;
    unsigned char *data;
    char msg[128];

    sprintf(path, "%s/%d/%d/%d", root_dir, zoom, x, y);

    // at first, attempt to open for read to check if the tile already loaded
    f = fopen(path, "rb");
    if(f) {
        // tile already loaded, skip
        fclose(f);
        return;
    }

    snprintf(msg, sizeof(msg), "Loading tile:\nz=%d x=%d y=%d", zoom, x, y);
    dialog_show_message(msg);
    LOG("%s", msg);

    num_retries = 0;

    while(num_retries++ < MAX_RETRIES) {
        // Check if we need to (re)connect to the server
        if(s == INVALID_SOCKET) {
            if(sock_open() != EXIT_SUCCESS) {
                delay(num_retries * 1000);
                continue;
            }
        }

        // Send http GET request
        sprintf(get, FORMAT_GET, zoom, x, y, host);
        //LOG(">>> Request:\n%s", get);  // debug
        res = send(s, get, strlen(get), 0);
        if(res < 0) {
            LOG("send() failed with error: %d (%s)", errno, strerror(errno));
            sock_close();
            delay(num_retries * 1000);
            continue;
        }

        // Receive http response
        res = recv(s, buffer, DEFAULT_BUFLEN - 1, 0);
        if(res <= 0) {
            LOG("recv() failed with error: %d (%s)", errno, strerror(errno));
            sock_close();
            delay(num_retries * 1000);
            continue;
        }
        buffer[res] = '\0';
        //LOG("<<< Response:\n%s", buffer);   // debug

        // Verify http response header
        if(parse_http_response(&content_lenght, &data) != EXIT_SUCCESS) {
            sock_close();
            delay(num_retries * 1000);
            continue;
        }

        // Create output file
        f = fopen(path, "wb");
        if(!f) {
            LOG("Failed to create \"%s\"", path);
            sock_close();
            tiles_failed++;
            return; // do not retry
        }

        // If there are data after http header in the current buffer then write them to the file
        if(data - buffer < res) {
            int remaining_len = res - (data - buffer);
            if(fwrite(data, 1, remaining_len, f) != remaining_len) {
                LOG("Failed to write %d bytes", remaining_len);
                fclose(f);
                remove(path);
                sock_close();
                tiles_failed++;
                return; // do not retry
            }
            content_lenght -= remaining_len;
        }

        // Retrieve the rest of the tile
        while(content_lenght > 0) {
            res = recv(s, buffer, DEFAULT_BUFLEN, 0);
            if(res <= 0) {
                LOG("recv() failed with error: %d (%s)", errno, strerror(errno));
                fclose(f);
                //remove(path); // not needed here as the file to be re-create at the next retry
                sock_close();
                delay(num_retries * 1000);
                continue;
            }

            if(fwrite(buffer, 1, res, f) != res) {
                LOG("Failed to write %d bytes", res);
                fclose(f);
                remove(path);
                sock_close();
                tiles_failed++;
                return; // do not retry
            }
            content_lenght -= res;
        }

        // the tile successfully retrieved
        fclose(f);
        tiles_loaded++;
        return;
    }

    // Excided the limit of retries, give up
    remove(path);
    tiles_failed++;
}

/**
 * @brief Retrieving tiles to cover the rectange provided as argument
 *
 * @return none
 */
void load_rect_int(int_coord_t *lt, int_coord_t *rb, int zoom)
{
    int x, y;       // working coordinates

    sprintf(path, "%s/%d", root_dir, zoom);
    if(mkdir(path, S_IRWXU | S_IRWXG | S_IRWXO)) {
        if(errno != EEXIST) {
            LOG("mkdir \"%s\" failed, err=%d (%s)", path, errno, strerror(errno));
            mkdir_failed++;
            return;
        }
    }

    // Loop through the horizontal tiles at the current zoom level
    for(x = lt->x; x <= rb->x; x++) {
        sprintf(path, "%s/%d/%d", root_dir, zoom, x);
        if(mkdir(path, S_IRWXU | S_IRWXG | S_IRWXO)) {
            if(errno != EEXIST) {
                LOG("mkdir \"%s\" failed, err=%d (%s)", path, errno, strerror(errno));
                mkdir_failed++;
                continue;
            }
        }

        // Loop through the vertical tiles at the fixed horizontal tile and the current zoom level
        for(y = lt->y; y <= rb->y; y++) {
            load_tile(zoom, x, y);
        }
    }
}

/**
 * @brief Retrieving tiles to cover the rectange provided as argument
 *
 * @return none
 */
void load_rect(rect_t *rect)
{
    int_coord_t lt; // left top corner
    int_coord_t rb; // right bottom corner
    int zoom;       // current zoom level

    // convert left top corner coordinates from degrees to tiles at max provided zoom level
    lt.x = (int)(((rect->left_top.lon + 180.0) / 360.0) * (1 << rect->max_zoom));
    lt.y = (int)((1 - log(tan(rect->left_top.lat * M_PI / 180.0) + 1 / cos(rect->left_top.lat * M_PI / 180.0)) / M_PI) *
        (1 << (rect->max_zoom - 1)));

    // convert right bottom corner coordinates from degrees to tiles at max provided zoom level
    rb.x = (int)(((rect->right_bott.lon + 180.0) / 360.0) * (1 << rect->max_zoom));
    rb.y = (int)((1 - log(tan(rect->right_bott.lat * M_PI / 180.0) + 1 / cos(rect->right_bott.lat * M_PI / 180.0)) / M_PI) *
        (1 << (rect->max_zoom - 1)));

    // Loop through the provided zoom levels from max to min
    for(zoom = rect->max_zoom; zoom >= rect->min_zoom; zoom--) {
        LOG("\tzoom=%d tile rectangle (%d, %d)-(%d, %d)", zoom, lt.x, lt.y, rb.x, rb.y);
        load_rect_int(&lt, &rb, zoom);
        lt.x >>= 1;
        lt.y >>= 1;
        rb.x >>= 1;
        rb.y >>= 1;
    }
}

/**
 * @brief Retrieving tiles to cover the rectange provided as argument
 *
 * @return none
 */
void load_line(line_t *line)
{
    int_coord_t bgn;
    int_coord_t end;
    int_coord_t cur;

    // convert coordinates from degrees to tiles at the provided zoom level
    bgn.x = (int)(((line->beg.lon + 180.0) / 360.0) * (1 << line->zoom));
    bgn.y = (int)((1 - log(tan(line->beg.lat * M_PI / 180.0) + 1 / cos(line->beg.lat * M_PI / 180.0)) / M_PI) *
        (1 << (line->zoom - 1)));

    end.x = (int)(((line->end.lon + 180.0) / 360.0) * (1 << line->zoom));
    end.y = (int)((1 - log(tan(line->end.lat * M_PI / 180.0) + 1 / cos(line->end.lat * M_PI / 180.0)) / M_PI) *
        (1 << (line->zoom - 1)));

    int dx = end.x - bgn.x;
    int dy = end.y - bgn.y;

    if(dx == 0 && dy == 0) {
        // the line is reduced to a point
        //@TODO: handle it
        return;
    }

    int inc_x = (dx >= 0 ? 1 : -1);
    int inc_y = (dy >= 0 ? 1 : -1);

    LOG("From (%d,%d) to (%d,%d) dx=%d dy=%d inc(%d,%d)", bgn.x, bgn.y, end.x, end.y, dx, dy, inc_x, inc_y);

    int_coord_t lt; // left top corner
    int_coord_t rb; // right bottom corner
    int thickness = line->thickness / 2;
    if(dx * inc_x > dy * inc_y) {
        for(cur.x = bgn.x; cur.x != end.x; cur.x += inc_x) {
            cur.y = bgn.y + (cur.x - bgn.x) * dy / dx;
            LOG("\tcur.x=%d cur.y=%d", cur.x, cur.y);
            lt.x = cur.x - thickness;
            lt.y = cur.y - thickness;
            rb.x = cur.x + thickness;
            rb.y = cur.y + thickness;
            load_rect_int(&lt, &rb, line->zoom);
        }
    } else {
        for(cur.y = bgn.y; cur.y != end.y; cur.y += inc_y) {
            cur.x = bgn.x + (cur.y - bgn.y) * dx / dy;
            LOG("\tcur.x=%d cur.y=%d", cur.x, cur.y);
            lt.x = cur.x - thickness;
            lt.y = cur.y - thickness;
            rb.x = cur.x + thickness;
            rb.y = cur.y + thickness;
            load_rect_int(&lt, &rb, line->zoom);
        }
    }
}

/**
 * Program entry point
 */
int main(void)
{
    int i;

    bps_initialize();
    dialog_init();

    if(mkdir(root_dir, S_IRWXU | S_IRWXG | S_IRWXO)) {
        if(errno != EEXIST) {
            LOG("mkdir \"%s\" failed, err=%d (%s)", root_dir, errno, strerror(errno));
            return EXIT_FAILURE;
        }
    }

    // load rectangles
    for(i = 0; i < NUM_RECTS; i++) {
        char title[128];
        snprintf(title, sizeof(title), "Loading rectangle %d of %d", i + 1, NUM_RECTS);
        dialog_set_title(title);
        LOG("%s", title);
        load_rect(&rect[i]);
    }

    // load lines
    for(i = 0; i < NUM_LINES; i++) {
        char title[128];
        snprintf(title, sizeof(title), "Loading line %d of %d", i + 1, NUM_LINES);
        dialog_set_title(title);
        LOG("%s", title);
        load_line(&line[i]);
    }

    sock_close();

    // Log statistics
    char msg[512];
    snprintf(msg, sizeof(msg), "Succesfully loaded: %d tiles\n", tiles_loaded);
    if(tiles_failed) {
        snprintf(msg + strlen(msg), sizeof(msg) - strlen(msg), "Failed to load: %d tiles\n", tiles_failed);
    }
    if(mkdir_failed) {
        snprintf(msg + strlen(msg), sizeof(msg) - strlen(msg), "Failed to create folder: %d times\n", mkdir_failed);
    }

    dialog_set_title("Statistics:");
    dialog_show_message(msg);
    LOG("%s", msg);
    delay(5000);

    bps_shutdown();

    return EXIT_SUCCESS;
}

